﻿//===================================================================================
// Copyright (c) Ryan Garrett.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===================================================================================

//WRITE: After deserialization, the ValidationErrors property could be null if previously it had a count of 0
//WRITE: Remember to document that if a property does not support the SignalsIsDirty property, the SignalsParentIsDirty will not work.  Need to test this.  The same is true for SignalsIsInvalid and SignalsParentIsInvalid
//WRITE: While technically you can have both wrapped and non-wrapped properties on a model, it has not been tested and is not reccommended
//FUTURE: When fleshing out the deserialization process, make sure there is logic that accounts for unconstrained properties.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Caffeine.Core.Metadata;

namespace Caffeine.Core
{
    /// <summary>
    /// Base class for all model properties
    /// </summary>
    public abstract class ModelPropertyBase : INotifyPropertyChanged
    {
        #region State

        private Boolean _isDirty;
        private Boolean _isInvalid;
        private PropertyChangedEventHandler _propertyChanged;
        private IEnumerable<ValidationError> _validationErrors;
        private readonly Nullable<ModelPropertyBehaviors> _behavior;
        private readonly ModelBase _parent;
        private readonly String _name;
        private readonly Boolean _isWrapped;
        private readonly String _wrappedPropertyName;
        private readonly String _isDirtyWrappedName;
        private readonly String _isInvalidWrappedName;
        private readonly String _validationErrorsWrappedName;
        
        #endregion

        #region Constructors

        /// <summary>
        /// Type constructor
        /// </summary>
        static ModelPropertyBase()
        {
            PropertyStringLength = "Property".Length;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        internal ModelPropertyBase
        (
            ModelBase parent, 
            String name, 
            Nullable<ModelPropertyBehaviors> behavior, 
            Boolean isWrapped
        )
        {
            _parent = parent;
            _name = name;
            _behavior = behavior;
            _isWrapped = isWrapped;

            if (IsWrapped)
            {
                if (!IsModelCollectionProperty)
                {
                    _wrappedPropertyName = Name.Remove(Name.Length - PropertyStringLength);
                }
                else
                {
                    _wrappedPropertyName = Name;
                }

                _isDirtyWrappedName = String.Format("{0}{1}", WrappedPropertyName, IsDirtyPropertyName);
                _isInvalidWrappedName = String.Format("{0}{1}", WrappedPropertyName, IsInvalidPropertyName);
                _validationErrorsWrappedName = String.Format("{0}{1}", WrappedPropertyName, ValidationErrorsPropertyName);
            }
        }

        #endregion

        #region Public

        /// <summary>
        /// Raised whenever an observable property is changed
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged
        {
            add
            {
                _propertyChanged += value;
            }
            remove
            {
                _propertyChanged -= value;
            }
        }

        /// <summary>
        /// Gets the parent of this property. This property supports INPC.
        /// </summary>
        public ModelBase Parent
        {
            get
            {
                return _parent;
            }
        }

        /// <summary>
        /// Gets the name of this property
        /// </summary>
        public String Name
        {
            get
            {
                return _name;
            }
        }

        /// <summary>
        /// Gets whether or not this property is wrapped
        /// </summary>
        public Boolean IsWrapped
        {
            get
            {
                return _isWrapped;
            }
        }

        /// <summary>
        /// Gets the behaviors of this property
        /// </summary>
        public Nullable<ModelPropertyBehaviors> Behavior
        {
            get
            {
                return _behavior;
            }
        }

        /// <summary>
        /// Gets whether or not this property is dirty. This property supports INPC.
        /// </summary>
        public Boolean IsDirty
        {
            get
            {
                return _isDirty;
            }
            protected set
            {
                if (_isDirty != value)
                {
                    _isDirty = value;
                    RaiseNotifyPropertyChanged("IsDirty");
                    Parent.RaiseOnPropertyIsDirtyChanged(this);

                    if (this.CheckSetsParentIsDirty())
                    {
                        Parent.SignalIsDirtyChange(value);
                    }
                }
            }
        }

        /// <summary>
        /// Gets whether or not this property is invalid. This property supports INPC.
        /// </summary>
        public Boolean IsInvalid
        {
            get
            {
                return _isInvalid;
            }
            protected set
            {
                if (_isInvalid != value)
                {
                    _isInvalid = value;
                    RaiseNotifyPropertyChanged("IsInvalid");
                    Parent.RaiseOnPropertyIsInvalidChanged(this);

                    if (this.CheckSetsParentIsInvalid())
                    {
                        Parent.SignalIsInvalidChange(value);
                    }
                }
            }
        }

        /// <summary>
        /// Gets the type of value contained by this property
        /// </summary>
        public abstract Type PropertyType
        {
            get;
        }

        /// <summary>
        /// Gets whether or not this property can contain a single child model
        /// </summary>
        public abstract Boolean IsModelProperty
        {
            get;
        }

        /// <summary>
        /// Gets whether or not this property can contain a collection of child models
        /// </summary>
        public abstract Boolean IsModelCollectionProperty
        {
            get;
        }

        /// <summary>
        /// Collection of errors found during validation
        /// </summary>
        public IEnumerable<ValidationError> ValidationErrors
        {
            get
            {
                return _validationErrors;
            }
            protected set
            {
                if (_validationErrors != value)
                {
                    _validationErrors = value;
                    RaiseNotifyPropertyChanged("ValidationErrors");
                }
            }
        }

        /// <summary>
        /// Reset this property to its last committed state
        /// </summary>
        public void Reset()
        {
            OnReset();
        }

        /// <summary>
        /// Release any event handlers registered to receive property change notifications
        /// </summary>
        public virtual void ReleaseObservers()
        {
            _propertyChanged = null;
        }

        /// <summary>
        /// Assign this property to its default value
        /// </summary>
        public abstract void AssignDefaultValue();

        /// <summary>
        /// Commit the current value of this property
        /// </summary>
        public abstract void Commit();

        /// <summary>
        /// Validate this property
        /// </summary>
        public void Validate()
        {
            if (this.CheckSetsIsInvalid())
            {
                if (CheckIsValidationMethodSet())
                {
                    Boolean isValid = false;
                    IEnumerable<ValidationError> validationFailureReasons = null;
                    Validate(ref validationFailureReasons, ref isValid);

                    if (isValid)
                    {
                        ValidationErrors = null;
                    }
                    else if (!CheckAreValidationErrorsEqual(ValidationErrors, validationFailureReasons))
                    {
                        ValidationErrors = validationFailureReasons;
                    }

                    IsInvalid = !isValid;
                }
                else
                {
                    ValidationErrors = null;
                    IsInvalid = false;
                }
            }
        }

        /// <summary>
        /// Raise a property change notification event on this model property
        /// </summary>
        /// <param name="propertyName">Name of property to raise event for</param>
        public virtual void RaiseNotifyPropertyChanged(String propertyName)
        {
            RaisePropertyChangedInternal(propertyName);

            if (IsWrapped)
            {
                switch (propertyName)
                {
                    case IsDirtyPropertyName:
                        Parent.RaisePropertyChanged(IsDirtyWrappedName);
                        break;
                    case IsInvalidPropertyName:
                        Parent.RaisePropertyChanged(IsInvalidWrappedName);
                        break;
                    case ValidationErrorsPropertyName:
                        Parent.RaisePropertyChanged(ValidationErrorsWrappedName);
                        break;
                }
            }
        }

        #endregion

        #region Internal

        internal String IsDirtyWrappedName
        {
            get
            {
                return _isDirtyWrappedName;
            }
        }

        internal String IsInvalidWrappedName
        {
            get
            {
                return _isInvalidWrappedName;
            }
        }

        internal String ValidationErrorsWrappedName
        {
            get
            {
                return _validationErrorsWrappedName;
            }
        }

        internal String WrappedPropertyName
        {
            get
            {
                return _wrappedPropertyName;
            }
        }

        internal Object GetPropertyChangedForTesting()
        {
            return _propertyChanged;
        }

        internal abstract void MergeAdjacentProperty(ModelPropertyBase property);

        #endregion

        #region Protected

        protected abstract void OnReset();

        protected ValidationErrorMetadata[] GetValidationErrorMetadata()
        {
            ValidationErrorMetadata[] validationErrorMetadata = null;

            if (ValidationErrors != null)
            {
                Int32 errorCount = ValidationErrors.Count();

                if (errorCount > 0)
                {
                    validationErrorMetadata = new ValidationErrorMetadata[errorCount];
                    Int32 index = 0;

                    foreach (ValidationError error in ValidationErrors)
                    {
                        validationErrorMetadata[index++] = new ValidationErrorMetadata()
                        {
                            ErrorCode = error.ErrorCode,
                            ErrorDescription = error.ErrorDescription
                        };
                    }
                }
            }

            return validationErrorMetadata;
        }

        protected void ApplyPropertyMetadata(ModelPropertyBaseMetadata metadata)
        {
            _isDirty = metadata.IsDirty;
            _isInvalid = metadata.IsInvalid;

            if (metadata.ValidationErrors != null)
            {
                List<ValidationError> errors = new List<ValidationError>();

                foreach (ValidationErrorMetadata error in metadata.ValidationErrors)
                {
                    errors.Add
                    (
                        new ValidationError()
                        {
                            ErrorCode = error.ErrorCode,
                            ErrorDescription = error.ErrorDescription
                        }
                    );
                }

                _validationErrors = errors;
            }
        }

        protected abstract Boolean CheckIsValidationMethodSet();

        protected abstract void Validate(ref IEnumerable<ValidationError> validationFailureReasons, ref Boolean isValid);

        #endregion

        #region Private

        private static readonly Int32 PropertyStringLength;

        private const String IsDirtyPropertyName = "IsDirty";

        private const String IsInvalidPropertyName = "IsInvalid";

        private const String ValidationErrorsPropertyName = "ValidationErrors";

        private void RaisePropertyChangedInternal(String propertyName)
        {
            if (_propertyChanged != null)
            {
                _propertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        private static Boolean CheckAreValidationErrorsEqual(IEnumerable<ValidationError> errorCollectionA, IEnumerable<ValidationError> errorCollectionB)
        {
            Boolean areErrorCollectionsEqual = Object.ReferenceEquals(errorCollectionA, errorCollectionB);

            if (!areErrorCollectionsEqual && errorCollectionA != null && errorCollectionB != null)
            {
                areErrorCollectionsEqual = errorCollectionA.Count() == errorCollectionB.Count();

                if (areErrorCollectionsEqual)
                {
                    for (Int32 index = 0; index < errorCollectionA.Count(); index++)
                    {
                        ValidationError errorA = errorCollectionA.ElementAt(index);
                        ValidationError errorB = errorCollectionB.ElementAt(index);
                        areErrorCollectionsEqual = Object.ReferenceEquals(errorA, errorB);

                        if (!areErrorCollectionsEqual)
                        {
                            if (errorA != null && errorB != null)
                            {
                                areErrorCollectionsEqual = errorA.Equals(errorB);
                            }
                        }

                        if (!areErrorCollectionsEqual)
                        {
                            break;
                        }
                    }
                }
            }

            return areErrorCollectionsEqual;
        }

        #endregion
    }
}
